/*

Copyright 2009 Nicola Dal Pont

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

*/

package com.astoolkit.workflow
{
	import com.astoolkit.workflow.constant.FailurePolicy;
	import com.astoolkit.workflow.constant.NO_DESCRIPTION;
	import com.astoolkit.workflow.constant.TaskStatus;
	import com.astoolkit.workflow.util.DataWrapper;
	
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.events.IEventDispatcher;
	import flash.net.registerClassAlias;
	import flash.utils.ByteArray;
	import flash.utils.Dictionary;
	import flash.utils.getQualifiedClassName;
	import flash.utils.setTimeout;
	
	import mx.binding.BindingManager;
	import mx.binding.utils.BindingUtils;
	import mx.binding.utils.ChangeWatcher;
	import mx.core.IFactory;
	import mx.core.mx_internal;
	import mx.events.PropertyChangeEvent;
	import mx.events.PropertyChangeEventKind;
	import mx.logging.ILogger;
	import mx.logging.Log;
	import mx.rpc.Fault;
	import mx.utils.ObjectProxy;
	import mx.utils.ObjectUtil;
	import mx.utils.UIDUtil;
	
	use namespace mx_internal;
	[Exclude(name="$", kind="property")]
	[Exclude(name="$data", kind="property")]
	[Exclude(name="$self", kind="property")]
	[Exclude(name="delegate", kind="property")]
	[Exclude(name="currentProgress", kind="property")]
	[Exclude(name="context", kind="property")]
	[Exclude(name="parent", kind="property")]
	/**
	 * sent only once, when the root workflow begins.
	 */
	[Event(
		name="initialized",
		type="com.astoolkit.workflow.WorkflowEvent")]
	[Event(
		name="prepared",
		type="com.astoolkit.workflow.WorkflowEvent")]
	/**
	 * sent once the task's pipeline is set
	 * 
	 * @eventType com.astoolkit.workflow.WorkflowEvent.DATA_SET 
	 */
	[Event(
		name="dataSet",
		type="com.astoolkit.workflow.WorkflowEvent")]
	[Event(
		name="started",
		type="com.astoolkit.workflow.WorkflowEvent")]
	[Event(
		name="warning",
		type="com.astoolkit.workflow.WorkflowEvent")]
	[Event(
		name="fault",
		type="com.astoolkit.workflow.WorkflowEvent")]
	[Event(
		name="completed",
		type="com.astoolkit.workflow.WorkflowEvent")]
	[Event(
		name="progress",
		type="com.astoolkit.workflow.WorkflowEvent")]
	[Event(
		name="aborted",
		type="com.astoolkit.workflow.WorkflowEvent")]

	[Bindable]
	public class BaseTask 
		extends EventDispatcher
		implements IWorkflowTask
	{
		use namespace mx_internal;
		
		private static const LOGGER : ILogger = 
			Log.getLogger( getQualifiedClassName( BaseTask ).replace(/:+/g, "." ) );
		
		/**
		 * @private
		 */
		protected var _document : Object;
		
		protected var _thread : uint;
		/**
		 * @private
		 */
		private var _root : IWorkflow;
		
		/**
		 * @private
		 */
		protected var _parent : IWorkflow;
		/**
		 * @private
		 */
		protected var _description : String= NO_DESCRIPTION;
		/**
		 * @private
		 */
		protected var _enabled : Boolean = true;
		/**
		 * @private
		 */
		protected var _failurePolicy : String = FailurePolicy.ABORT;
		/**
		 * @private
		 */
		protected var _currentProgress : Number = -1;
		/**
		 * @private
		 */
		protected var _delay : int;
		/**
		 * @private
		 */
		protected var _delegate : IWorkflowDelegate;
		/**
		 * @private
		 */
		protected var __status : String;
		/**
		 * @private
		 */
		protected var _context : IWorkflowContext;
				
		/**
		 * @private
		 */
		private var _resumeCallBacks : Array;
		
		/**
		 * @private
		 */
		private var _suspendableListeners : Object;
		
		public var $self : *;

		public function get $() : *
		{
			return context.variables;
		}
		
		public function set $( inValue : * ) : void
		{
			
		}
		
		public function BaseTask()
		{
			super();
			_status = TaskStatus.STOPPED;
		}
				
		public function get async() : Boolean
		{
			return true;
		}
		
		public function get context() : IWorkflowContext
		{
			return _context;
		}
		
		public function set context( inContext : IWorkflowContext ) : void
		{
			_context = inContext;
		}
		
		protected function get _status() : String
		{
			return __status;
		}
		
		protected function set _status( inStatus : String ) : void
		{
			var oldValue : String = __status;
			__status = inStatus;
			dispatchEvent( 
				new PropertyChangeEvent( 
					"status_change",
					false,
					false,
					PropertyChangeEventKind.UPDATE,
					"status",
					oldValue,
					__status,
					this ) );

		}
		
		public function get status():String
		{
			return _status;
		}
		
		public function get description():String
		{
			return _description;
		}
		
		public function set description(inName:String):void
		{
			_description = inName;
		}
		
		public function get delay():int
		{
			return _delay;
		}
		
		public function set delay(inDelay:int):void
		{
			_delay = inDelay;
		}
		
		public function get enabled():Boolean
		{
			return _enabled;
		}
		
		public function set enabled(inEnabled:Boolean):void
		{
			_enabled = inEnabled;
		}
		
		
		[Inspectable(defaultValue="abort", enumeration="abort,warn,ignore,continue")]
		public function get failurePolicy() : String
		{
			return _failurePolicy;
		}
		
		override public function set failurePolicy( inPolicy : String ) : void
		{
			_failurePolicy = inPolicy;
		}
		
		public function get parent():IWorkflow
		{
			return _parent;
		}
		
		public function set parent(inParent:IWorkflow):void
		{
			_parent = inParent;
			if( _parent )
				parent.addEventListener( "status_change", onParentStatusChange );
				
		}
		
		public function get currentProgress():Number
		{
			return _currentProgress;
		}
		
		public function set currentProgress(inProgress:Number):void
		{
			_currentProgress = inProgress;
		}
		
		public function set delegate(inDelegate:IWorkflowDelegate):void
		{
			_delegate = inDelegate;
		}
		
		public function cleanUp():void
		{
			_status = TaskStatus.STOPPED;
			_context.removeEventListener( WorkflowEvent.SUSPENDED, onContextSuspended );
			_context.removeEventListener( WorkflowEvent.RESUMED, onContextResumed );
			
			for each( var array : Array in _suspendableListeners )
				for each( var listener : SuspendableListener in  array )
					listener.target.removeEventListener( listener.type, listener.handler );
			_suspendableListeners = [];
		}
		
		public function begin( inRetain : Boolean = false ) : void
		{	
			if( _status == TaskStatus.RUNNING )
				throw new Error( "begin() called while task already running: " + getQualifiedClassName( this ) );
			LOGGER.debug( "begin() '{0}' ({1})", description, getQualifiedClassName( this ) );
			_context.addEventListener( WorkflowEvent.SUSPENDED, onContextSuspended );
			_context.addEventListener( WorkflowEvent.RESUMED, onContextResumed );
			_context.runningTask = this;
			_status = TaskStatus.RUNNING;
			_delegate.onBegin( this );
			
		}

		public function get root() : IWorkflow
		{
			if( _root )
				return _root;
			var out : IWorkflowTask = this;
			while( out.parent != null )
			{
				out = out.parent;
			}
			_root = out as IWorkflow;
			return _root;
		}
		
		protected function setProgress( inValue : Number ) : void
		{
			var oldVal : Number = _currentProgress;
			_currentProgress = inValue;
			if( _currentProgress >= 0 && _currentProgress <= 1 )
			{
				_delegate.onProgress( this );
				dispatchEvent( 
					new PropertyChangeEvent(
						PropertyChangeEvent.PROPERTY_CHANGE,
						false, 
						true, 
						PropertyChangeEventKind.UPDATE, 
						"progress", 
						oldVal, 
						_currentProgress, 
						this) );
			}
			_delegate.onProgress( this );
		}
		
		protected function complete() : void
		{
			
			if( !root.async )
				_deferredComplete( _thread );
			else
				setTimeout( _deferredComplete, 1, _thread );
		}
		
		/**
		 * @private
		 */
		protected function _deferredComplete( inThread : uint ) : void
		{
			if( inThread != _thread )
				return;
			if( _status == TaskStatus.SUSPENDED )
			{
				addResumeCallBack( 
					function() : void
					{
						_deferredComplete( _thread );
					} );
				return;
			}
			_context.runningTask = null;
			_thread++;
			_delegate.onComplete( this );
			
		}

		protected function fail( inMessage : String, inFault : Fault = null ) : void
		{
			if( _status == TaskStatus.ABORTED )
				return;
			if( root.async )
				_deferredFail( inMessage, inFault, _thread );
			else
				setTimeout( _deferredFail, 1, inMessage, inFault, _thread );
		}

		/**
		 * @private
		 */
		protected function _deferredFail( inMessage : String, inFault : Fault, inThread : uint ) : void
		{
			if( inThread != _thread )
				return;
			
			if( _status == TaskStatus.SUSPENDED )
			{
				addResumeCallBack( 
					function() : void
					{
						_deferredFail( inMessage, inFault, inThread ); 
					} );
				return;
			}
			_thread++
			_status = TaskStatus.IDLE;
			LOGGER.info( 
				"Task {0}:\"{1}\" failed. Reason: {2} \n {3}", 
				getQualifiedClassName( this ),
				description,
				inMessage,
				inFault ? inFault.getStackTrace() : "" );
				
			_delegate.onFault( this, inMessage, inFault );
		}
		
		protected function initResumeCallBacks() : void
		{
			_resumeCallBacks = [];
		}
		
		protected function addResumeCallBack( inFunction : Function ) : void
		{
			if( !_resumeCallBacks )
				initResumeCallBacks();
			_resumeCallBacks.push( inFunction );
		}
		
		protected function invokeResumeCallBacks() : void
		{
			if( _resumeCallBacks != null )
			{
				for each( var fn : Function in _resumeCallBacks )
				{
					fn();
				}
				clearResumeCallBack();
			}
		}
		
		protected function clearResumeCallBack() : void
		{
			_resumeCallBacks = null;
		}
		
		public function initialize() : void
		{
			if( _status != TaskStatus.STOPPED )
				return;
			_status = TaskStatus.IDLE;
       		_delegate.onInitialize( this );
			if( _document == null )
				_document = this;
		}
		
		public function prepare() : void
		{
			if( parent == null )
				_document.$self = new DataWrapper(this);
			_suspendableListeners = {};
			_status = TaskStatus.IDLE;
			_delegate.onPrepare( this );
			initResumeCallBacks();
			

		}
		
		public function onParentStatusChange( inEvent : PropertyChangeEvent ) : void
		{
			if( inEvent.newValue == TaskStatus.ABORTED )
				_status = TaskStatus.ABORTED;
		}
		
		public function abort() : void
		{
			LOGGER.debug( "abort() '{0}' ({1})", description, getQualifiedClassName( this ) );
			_status = TaskStatus.ABORTED;
			_thread ++;
			_delegate.onAbort( this, "Aborted: " + description );
		}
		
		public function suspend() : void
		{
			if( _status != TaskStatus.SUSPENDED )
			{
				_status = TaskStatus.SUSPENDED;
				_delegate.onSuspend( this );
			}
		}
		
		public function resume() : void
		{
			if( _status != TaskStatus.RUNNING )
			{
				_status = TaskStatus.RUNNING;
				_delegate.onResume( this );
				invokeResumeCallBacks();
			}
		}
		
		protected function onContextSuspended( inEvent : WorkflowEvent ) : void
		{
			if( _status == TaskStatus.RUNNING )
			{
				suspend();
			}
		}
		
		protected function onContextResumed( inEvent : WorkflowEvent ) : void
		{
			if( status == TaskStatus.SUSPENDED )
			{
				resume();
			}
		}
		
		protected function addSuspendableListener( 
			inTarget : IEventDispatcher, 
			inType : String, 
			inHandler : Function, 
			inUseCapture : Boolean = false, 
			inPriority : uint = 0, 
			inUseWeakReference : Boolean = false ) : void
		{
			inTarget.addEventListener( inType, suspendableEventHandler, inUseCapture, inPriority, inUseWeakReference );
			if( !_suspendableListeners.hasOwnProperty( UIDUtil.getUID( inTarget ) + inType ) )
				_suspendableListeners[ UIDUtil.getUID( inTarget ) + inType ] = [];
			_suspendableListeners[ UIDUtil.getUID( inTarget ) + inType ].push(  new SuspendableListener( inHandler, _thread, inTarget, inType ) );
		}
		
		protected function removeSuspendableListener( 
			inTarget : IEventDispatcher, 
			inType : String, 
			inHandler : Function ) : void
		{
			inTarget.removeEventListener( inType, suspendableEventHandler);
			var fns : Array = _suspendableListeners[ UIDUtil.getUID( inTarget ) + inType ] as Array;
			if( fns == null )
				return;
			for( var i : int = 0; i< fns.length; i++ )
			{
				if( inHandler == fns[i].handler )
				{
					fns.splice( i, 1 );
					return;
				}
			}
			
		}
		
		/**
		 * @private
		 */
		private function suspendableEventHandler( inEvent : Event ) : void
		{
			var fns : Array = ( _suspendableListeners[ UIDUtil.getUID( inEvent.target ) + inEvent.type ] as Array);
			if( fns == null )
				return;
			fns = fns.concat();
			var newFns : Array = [];
			for each( var fn : SuspendableListener in fns )
			{
				if( fn.thread == _thread )
				{
					newFns.push( fn );
					var t : uint = _thread;
					if( _status == TaskStatus.SUSPENDED )
					{
						addResumeCallBack( 
							function() : void
							{
								if( fn.thread == t )
									fn.handler( inEvent );
								else
									removeSuspendableListener( fn.target, fn.type, fn.handler ); 
							} );
					}
					else
					{
						fn.handler( inEvent );
					}
				}
			}
		}
		/**
		 * implementation of IMXMLObject
		 */
		public function initialized( inDocument : Object, inId : String ) : void
		{
			_document = inDocument;
		}
				
	}
}
import flash.events.IEventDispatcher;

import mx.binding.Watcher;
import mx.binding.utils.BindingUtils;
import mx.core.mx_internal;

class SuspendableListener
{
	public function SuspendableListener( inHandler : Function, inThread : uint, inTarget : IEventDispatcher, inType : String ) 
	{
		thread = inThread;
		handler = inHandler;
		target = inTarget;
		type = inType;
	}
	
	public var handler : Function;
	public var thread : uint;
	public var target : IEventDispatcher;
	public var type : String;
}


/*
	INTERNAL FUNCTIONS TO PREVENT BINDINGS FROM HAPPEN 
	UNLESS APPLIED ON CURRENTLY RUNNING TASK.
	THIS APPROACH DOESN'T WORK AS WATCHERS
	FIRE UPDATES DESPITE Binding OBJECTS WITH isActive 
	SET TO false. INVESTIGATING


	private function switchWatchers() : void
	{
		var bindings : Array =  _document.mx_internal::_bindings as Array;
		//var watchers : Array =  _document.mx_internal::_watchers as Array;
		for each( var b : * in bindings )
		{
			var bindedObj : Object = getBindingObj( _document, b.destString );
			
			if( bindedObj.obj is IWorkflowTask )
			{
				
				if( bindedObj.obj == this )
				{
					mx.binding.BindingManager.enableBindings( _document, b.destString, true );
				}
				else
				{
					mx.binding.BindingManager.enableBindings( _document, b.destString, bindedObj.prop == "enabled" );
				}
			}
		}
	}
	
	
	private function getBindingObj( inRoot : Object, inPath : String ) : Object
	{
		var out : Object = {};
		var chain : Array = inPath.split( "." );
		out.prop = chain.pop();
		for each( var prop : String in chain )
			inRoot = inRoot[ prop ];
		out.obj = inRoot; 
		return out;
	}

*/